
# Copyright (c) 2014, Franklin Brauning V.  All rights reserved.
# 
#    El desarrollo inicial de esta utilidad fue producido
#    por Franklin Brauning <brauningbar@gmail.com>
# 
#    Si consideras que este codigo puede serte util, eres libre
#    de usarlo a conveniencia, pero no olvides mantener la licencia
#    y dar credito al autor.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
#   * Redistributions of source code must retain the above copyright notice, this
#     list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
# 
#   * Neither the name of the author nor the names of its contributors may
#     be used to endorse or promote products derived from this software without
#     specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import django.conf
from django.db import models
import copy

# check if we are on mysql at the beginning because tidymodels were initially
# developed working and testing with sqlite backend.
# I've found inconsistencies and bugs in the mysql backend that have to be
# worked around on several points
ISMYSQL='mysql' in django.conf.settings.DATABASES['default']['ENGINE']

# used to debug tidy-models
#
def trace(*a, **kw):
	print(*a, **kw)

_tracelen = 4
_traces = [None] * _tracelen
def microtrace(*a, **kw):
	global _traces
	m = " ".join(map(str, a))
	if kw:
		m += " " + str(kw)
	try:
		_traces.insert(0, _traces.pop(_traces.index(m)))
		print('*', end='', flush=True)
	except ValueError:
		_traces.insert(0, m)
		_traces = _traces[:_tracelen]
		print(m)

class AbstractException(Exception):
	''' base exception for tidy-models '''
	pass


def tdcached(isgetter=False):
	'''decorator constructor (wrap with a call: @tdcached()) to
	in-memory cache expensive method calls on any tidy-django-model.

	If wrapped method is a getter and there is a setter wrapped
	with tdsetter (see documentation), construct with isgetter=True.

	If there is a need to skip the cache invoke the wrapped method
	with cached=False.

	Cached data will be cleared when due (namely after .save),
	but there arent clever checks to detect invalid/obsolete values
	(not yet?). This shouldnt be a problem as model instances are
	short lived.
	'''
	def deco(func):
		msg = 'hit (tdgetter)' if isgetter else 'hit (tdcached)'
		fkey = func.__name__ if isgetter else func
		def w(self, *a, cached=True, **kw):
			k = (fkey, a, tuple(kw.items()))
			if cached:
				try:
					x = self._tdcache[k]
					self.microtrace(msg, k)
					return x
				except KeyError:
					pass
				except AttributeError:
					self._tdcache = {}
			x = self._tdcache[k] = func(self, *a, **kw)
			return x
		return w
	return deco

''' alias to define tdcached getters '''
tdgetter = tdcached(isgetter=True)

# TODO: cached=True sin implementar
def tdsetter(gettername, args=(), kwargs={}, updatecache=False,
             cached=False, enterinvalidated=False):
	'''declare that wrapped method is a setter related to an existing
	tdcached method. This simple facility is necessary to sanely
	signal updates to cached values. For this to work, the getter
	method must be constructed with isgetter=True (see tdcached docs)

	Construct declaring the actual getter name.	It is normally
	not necessary to link setter's output to itself in the cache,
	if this a wrong assumption, construct with cached=True.

	Internally invalidates the cached value. When accesing
	again with getter method the value will get cached again.
	Construct with updatecache=True to perform caching immediatly
	(this is probably always needed if both getter and setter
	are severily intertwined by shared state).

	The following example relies on the cache to store the value:

	  @tdgetter
	  def access_data(self):
	      ...
	      return data

	  @tdsetter('access_data', updatecache=True):
	  def set_data(self, data):
	      return data

	The invalidation acts on getter call whose arguments match
	args= and kwargs=. By default asume the getter is called with no
	arguments. Also note that the invalidation takes place depending
	on enterinvalidated=. By default it will happen after wrapped
	method invocation.
	'''
	k = (gettername, args, tuple(kwargs.items()))
	def deco(func):
		def w(self, *a, **kw):
			if not enterinvalidated:
				x = func(self, *a, **kw)
			try:
				self._tdcache.pop(k)
			except KeyError:
				self.microtrace('invalidation miss')
			except AttributeError:
				self.microtrace('invalidation miss')
				self._tdcache = {}
			else:
				self.microtrace('invalidation (tdcached)', k)
			if enterinvalidated:
				x = func(self, *a, **kw)
			if updatecache:
				self._tdcache[k] = x
			return x
		return w
	return deco

class abstractmodel(models.Model):
	''' base model for tidy-models '''
	class Meta:
		abstract = True

	def trace(self, *a, **kw):
		trace('%s:' % self.__class__.__name__, *a, **kw)

	def microtrace(self, *a, **kw):
		microtrace('%s:' % self.__class__.__name__, *a, **kw)

	def cleantdcache(self):
		''' clears cache (tdcached) '''
		self._tdcache = {}

	def reload(self, updateobj=True):
		''' get the current register from database.

		When updateobj=True, the loaded data is assigned to the
		current object.

		Returns the loaded register
		'''
		c = self.__class__.objects.get(id=self.id)
		if updateobj:
			self.__dict__ = c.__dict__
			self.modified = False
		return c

	def nicer(self, tag='span'):
		''' emits a span html block for this register. 
		The model is required to define .title and .nice methods.

		.nice: should return the simplest way to represent the
		register as a string
		.title: supposed to provide extended data about the register.
		'''
		return "<%s title='%s'>%s</%s>" % (tag, self.title(),
		                                   self.nice(), tag)

	def ensureid(self, id):
		''' safer way to explicitly set the id of a register.
		Use this to not conflict or corrupt the tidy-models
		'''
		self._id = id

	def dup(self):
		''' generate a copy of the actual register. After .save
		the register will have the same properties but wont be
		the same '''
		y = copy.copy(self)
		y.id = None
		y.cleantdcache()
		return y

	def markunsafe(self):
		'''
		Use this signal to indicate that the current is modified
		but not with the intent of saving it but to present it.
		If any code after this call tries to .save the object,
		it probably shouln't
		'''
		self._unsafe = None

	def _checkunsafe(self):
		if hasattr(self, '_unsafe'):
			raise AbstractException("abstractmodel._checkunsafe: the currect object is marked unsafe and modifying it could produce corruption in the overall model")

	def _checkpresave(self):
		if hasattr(self, 'presave'):
			self.presave()
		self.cleantdcache()
		self.microtrace('abstractmodel: performing saving')

	def save(self, *a, **kw):
		self._checkunsafe()
		self._checkpresave()
		try:
			self.id = self.__dict__.pop('_id')
		except KeyError:
			pass
		return super(abstractmodel, self).save(*a, **kw)

	def rawsave(self, *a, **kw):
		''' util to apply the stock .save.
		No model should override this
		'''
		self._checkunsafe()
		self._checkpresave()
		try:
			self.id = self.__dict__.pop('_id')
		except KeyError:
			pass
		return super(abstractmodel, self).save(*a, **kw)

	def rawdelete(self):
		''' util to apply the stock .delete.
		No model should override this
		'''
		self._checkunsafe()
		self._checkpresave()
		super(abstractmodel, self).delete()

# The following decorators provide a composable way to enrich
# the __str__ method or a richer string representation of a model
#
# If you want to use this, the selected decorators have to be wrapped
# around the initializer decorator (.showinit) and some of the
# final decorators provided (.show, .show_comma, .show_comma_desc)
#

def showinit(func):
	''' inicial decorator that allows to compose several decorators
	providing extra representations about a model.
	Useful to extend __str__-like methods
	'''
	def w(self):
		return func(self), []
	return w

def show(func):
	''' final __str__ decorator styled-up to easy debug tidy-models.

	This decorator emits the final representation of composed
	representations of a model.
	'''
	def w(self):
		s, attrs = func(self)
		if attrs:
			return "%s [%s]" % (s, ", ".join(attrs))
		return s
	return w

def show_comma(func):
	''' final __str__ decorator styled-up in the form 

	x, y, z, ...

	The wrapped __str__ method result is ignored

	This decorator emits the final representation of composed
	representations of a model.
	'''
	def w(self):
		s, attrs = func(self)
		if attrs:
			return ", ".join(attrs)
		return s
	return w

def show_comma_desc(func):
	''' final __str__ decorator styled-up in the form

	repr:x, y, z, ...

	where repr is the string returned in the __str__-like method

	This decorator emits the final representation of composed
	representations of a model.
	'''
	def w(self):
		s, attrs = func(self)
		if attrs:
			return "%s: %s" % (s, ", ".join(attrs))
		return s
	return w

def show_table(*headers, colon=':'):
	''' final decorator '''
	def fff(func):
		def w(self):
			s, attrs = func(self)
			s = "<i>%s</i>" % s if s else ""

			classes = ['show_table']
			if getattr(self, 'deleted', None):
				classes.append("_deleted")
			else:
				classes.append("_active")

			if attrs:
				items = zip(headers, reversed(attrs))
				if colon:
					_row = "<td class=show_table_header>%s</td><td>" + \
					       colon + "</td><td>%s</td>"
				else:
					_row ="<td class=show_table_header>%s</td><td>%s</td>"
				lines = (_row % (x,y) for x,y in items)
				tablines =  ("<tr>%s</tr>" % l for l in lines)
				return "<div class='%s'>%s<p><table class=table_show_table border=0>%s</table></div>" % (
				  " ".join(classes), s, "".join(tablines))

			return "<div class='%s'>%s</div>" % (" ".join(classes), s)
		return w
	return fff

def show_desc(*headers, horizontal=True):
	''' final decorator.
	Make a bootstrap description paragraph
	'''
	def fff(func):
		def w(self):
			s, attrs = func(self)
			classes = []
			if getattr(self, 'deleted', None):
				classes.append("_deleted")
			else:
				classes.append("_active")

			if attrs:
				items = zip(headers, reversed(attrs))
				lines = ("<dt>%s</dt><dd>%s</dd>" % (x,y) for x,y in items)
				contents = "<dl %s>%s</dl>" % (
				  "class='dl-horizontal'" if horizontal else "",
				  "".join(lines))
			else:
				contents = ""
			if s:
				return "<span class='%s'><span class='lead'>%s</span>%s</span>" % (" ".join(classes), s, contents)
			else:
				return "<span class='%s'>%s</span>" % (" ".join(classes), contents)
		return w
	return fff

def show_header(*headers, colon=': ', align=None):
	''' final decorator.
	Similar to show_table but outputs text with one key:value for line
	formatted like:

	key1: value1
	key2: value2

	With align -1, align al values up to the column where the largest
	key reaches.
	With align -2, align values up to the column where the key with
	the median length of all reaches.
	With align n, align values at the indicated column
	'''
	headers = list(headers)

	def formatall(p1, rows):
		if p1:
			return p1 + "\n" + "\n".join(rows)
		return "\n".join(rows)

	def f(func):
		nonlocal align
		nonlocal headers
		if align==-1:
			align = max(map(len, headers))
		elif align==-2:
			align = int(sum(map(len,headers)) / len(headers))

		if type(align) is int:
			def w(self):
				s, attrs = func(self)
				rows = []
				for k,v in zip(headers, reversed(attrs)):
					kk = k + colon
					rows.append(kk + ' '*(2+align-len(kk)) + str(v))
				return formatall(s, rows)
		else:
			def w(self):
				s, attrs = func(self)
				rows = ["".join((k,colon,str(v))) for k,v in zip(headers,reversed(attrs))]
				return formatall(s, rows)
		return w
	return f



def show_table_row(func):
	''' final decorator '''
	def w(self):
		s, attrs = func(self)
		return "".join("<td>%s</td>" % x for x in [s]+attrs)
	return w

def showthis(f):
	''' composable decorator '''
	def fff(func):
		def w(self):
			s, attrs = func(self)
			attrs.append(f(self))
			return s,attrs
		return w
	return fff

def show_id(func):
	''' composable __str__ decorator showing id of register '''
	def w(self):
		s,ats = func(self)
		ats.append(str(self.id))
		return s,ats
	return w

def show_tdcached(func):
	''' composable __str__ decorator showing cached methods in register '''
	def w(self):
		if hasattr(self, '_tdcache'):
			s,ats = func(self)
			ats.append("tdcached(%s)" % ",".join(self._tdcache.keys()))
			return s,ats
		return func(self)
	return w
